#!/usr/local/bin/ruby
#
#  Copyright (c) 2009 Mu Dynamics, Inc. All rights reserved.

require 'rubygems'
require 'net/https'
require "xml/libxml"
require 'uri'
require 'json'
require 'lib/api/rest/rest_client_utils'
# require 'open3'
# require 'curb'
# require 'net/http'


include LibXML
include RestClientUtils

$DEBUG = true

$script_dir = ENV['TESTBRANCH'] + "/lib/script"

=begin
first, create a session context. In it we can load a scenario, set its hosts,
channels and options

Next, create a test context. In it we can verify a scenario (run), run
a functional test suite (test_suite_run), and collect test results

If we need to make changes to the scenario, we need to teardown_test to return
to the session context, make the changes (set_hosts. set_options, set_channels),
and then call setup_test to create a new test context

Finally, we teardown_test and close_session

  basic order of operations:
@api = DdtApi.new

@api.new_session   # new session context
   @api.load_scenario(scenario_uuid)
   @api.set_hosts (required unless scenario template has the hosts you want) (host_roles_array, host_names_array)
      @api.set_channels (optional)
      @api.set_options (optional, to add or change scenario options)
@api.setup_test  # takes the configured scenario and builds test context
   @api.run # to verify
   @api.run_test_suite(testsuite_uuid) # loads and runs test suite
   @api.get_test_suite_status  # poll until done
   @api.get_test_suite_results
   @api.teardown_test  # to return to the session context, tears down the test context
@api.close_session # tears down the session context
=end

=begin
   new_session    /ddt/sessions/newSession   POST/DELETE/GET  (get sessionId)
   load_scenario  /ddt/sessions/{sessionId}/loadScenario/{uuid}  POST
   get_hosts      /ddt/sessions/{sessionId}/scenario/hosts  GET   (get roles)
   set_hosts      /ddt/sessions/{sessionId}/scenario/hosts/{role}/{name}  POST
   get_channels   /ddt/sessions/{sessionId}/scenario/channels  GET  (get channels)
   set_channels   /ddt/sessions/{sessionId}/scenario/channels/{role}/{name}  POST
   get_options    /ddt/sessions/{sessionId}/scenario/options   GET (get options)
   set_options    /ddt/sessions/{sessionId}/scenario/options/{name}/{value}  POST
   setup_test     /ddt/sessions/{sessionId}/setupTest    POST

?   export_csv     /ddt/sessions/{sessionId}/test/export/csv/{uuid}  POST (csv file name? uuid?)
?   import_csv     /ddt/sessions/{sessionId}/test/import/csv/{filename}  POST (post multipart data?)
   run            /ddt/sessions/{sessionId}/test/run    POST (will run scenario)
   run_test_suite       /ddt/sessions/{sessionId}/test/runSuite/{uuid}  POST  (runs test suite)
   get_test_suite_results  /ddt/sessions/{sessionId}/test/runSuite/results   GET  (get results)
   get_test_suite_status   /ddt/sessions/{sessionId}/test/runSuite/status    GET (get status)

   teardown_test       /ddt/sessions/{sessionId}/teardownTest
   close_session        /ddt/sessions/{sessionId}/closeSession
   close_all_sessions        /ddt/sessions/{sessionId}/closeAllSessions

where is get_scenario_status ??? if run is called
=end

class DdtApi < ActiveRecord::Base
  attr_accessor :host, :docroot, :cookie, :username, :password, :session_id, :results, :posted_uuid, :testsuite, :results, :failed, :errors
  def initialize(host=ENV["MU_IP"], docroot = "/api/v5/ddt/")
    @host = host
    @username  = ENV['MU_ADMIN_USER'] || "admin"
    @password  = ENV['MU_ADMIN_PASS'] || "admin"
    @docroot = docroot
    @cookie = ''
    @testsuite = ''
    @testset = ''
    @response = nil
    @session_id = nil
    @results = Array.new
    logger.debug "Created DdtApi object to :#{@host}"
  end

  # scenario verify
  def run
    response = post("session/test/run")
    return response["testResult"]["status"]
  end

  # sets and executes test suite
  def run_test_suite(uuid=@testsuite)
     @testsuite = uuid
     response = post("session/test/runSuite/#{uuid}")
     return response["response"]["status"]
  end

  # must be called first to establish session
  def new_session
    reply = post("newSession") # with no args
    response = reply["response"]
    @session_id = response["sessionId"]
  end

  def close_session
    return get("session/closeSession")
  end

  # returns array of session_ids
  def get_sessions
    all_sessions = Array.new
    response = get()  # with no args
    sessions = response["sessions"]
    if !sessions["session"].nil?
      num = sessions["session"].size
      if num > 1
        sessions["session"].each do | s |
          all_sessions << s["id"]
        end
      else # special case if there is only one (there is no array)
        all_sessions << sessions["session"]["id"]
      end
      return all_sessions
    end
    return nil
  end

  def close_all_sessions
    response = post("closeAllSessions")
    return response["response"]["status"]
  end

  def get_all_sessions
    response = get("getAllSessions")
    return response["response"]["status"]
  end

  def setup_test
    response = post("session/setupTest")
    return response["response"]["status"]
  end

  def teardown_test
    response = post("session/test/tearDownTest")
    return response["response"]["status"]
  end

  def load_scenario(uuid)
    response = post("session/loadScenario/#{uuid}")
    return response["response"]["status"]
  end

  # returns array of host hashmaps, e.g.
  # [{"role"=>"192.168.30.188 (A1.V4)", "roleId"=>"host_0", "layer"=>"v4"}, {"role"=>"192.168.30.9 (dell-eth1.V4)", "roleId"=>"host_1", "layer"=>"v4"}]
  # NOTE: the values of 'roleId' are what are passed to set_hosts as 'roles', not 'role'
  def get_hosts
    response = get("session/scenario/hosts")
    hosts = response["hosts"]["host"]
    return hosts
  end

  # takes arrays of roles and names, e.g. ["h1", "h2"], ["a1", dell-9"]
  def set_hosts(roles, names)
    ok = true
    hosts = roles.size.to_i
    logger.debug "num hosts to set: #{hosts}"
    hosts.times do | i |
       response = post("session/scenario/hosts/#{roles[i]}/#{names[i]}")
       if response.nil?
         logger.debug  "set_hosts returned nil"
       end
       logger.debug  response
       if !response["response"]["message"].include?("Bind host failed")
         ok = false
       end
    end
    return ok
  end

  def get_channels
    response = get("session/scenario/channels")
    channels = response["channels"]["channel"]
    return channels
  end

  # takes arrays of roles and names]
  def set_channels(roles, names)
    response = ""
    roles.size.times do | i |
        response = post("session/scenario/channels/#{roles[i]}/#{names[i]}")
    end
    return response["response"]["message"]
  end

  # returns array of options hashmap, consisting of name and value keys, e.g.
  # [{"name"=>"io.timeout", "value"=>250}, {"name"=>"io.delay", "value"=>0}]
  def get_options
    response = get("session/scenario/options")
    options = response["options"]["option"]
    return options
  end

  # takes arrays of options names and values
  def set_options(names, values)
    response = ""
    names.size.times do | i |
       response = post("session/scenario/options/#{names[i]}/#{values[i]}")
    end
    return response["response"]["message"]
  end

  def collect_results(timeout=120)
    wait_until_done(timeout)
    results, failed = get_test_suite_results
    return results, failed, errors
  end

  def wait_until_done(timeout=120)
    finished = false
    interval = timeout / 10
    10.times do
      begin
        response = get_test_suite_status
        logger.debug "wait_until_done, response = #{response}"
        if !response.nil?
          if response.to_s.include?("Done")
            finished = true
            return finished
          end
        end
      rescue Exception => e
        logger.debug e # status may not be ready right away. may return a 500
      end
      sleep interval
    end
    return finished
  end

  def get_test_suite_status
    response = get("session/test/runSuite/status")
    logger.debug "get_test_suite_status response = #{response}"
    message = response["response"]["message"]
    return message
  end

  # @results should be an array of hashes, each item containing
  # test_name, test_options, test_result, and also test_error if test_result=FAILED
  def get_test_suite_results
    @failed = Hash.new
    @errors = Array.new
    response = get("session/test/runSuite/results")
    # logger.debug response
    @results = response["testResults"]["results"]
    # logger.debug @results
    @results.each do | r |
      str = "#{r["testName"]} = #{r["testResult"]}"
      logger.debug str
      if r["testResult"] == "FAILED"
        name = r["testName"]
        # logger.debug "adding #{name} to the failed array"
        @failed[name] = r["testResult"]
        @errors << r["testError"]
      end
    end
    return @results, @failed, @errors
  end

  def display_results()
   logger.debug @results
  end

  def csv_export(uuid=@testsuite)
    logger.debug uuid
    str = "session/test/export/csv/#{uuid}"
    logger.debug str
    response = post(str)
    return response["response"]["message"]
  end

  # returns posted uuid
 # def csv_import(filepath, content_type)
 #   response = post_template(filepath, "/api/v5/ddt/session/test/import/csv", content_type)
 #   return response["response"]["message"]
 # end

  # ----------------------------- utility methods ---------------------x

  # get - with no params, returns the entire netconfig json object
  def get(e="")
    uri = URI.parse("https://#{@host}/")
    @http = Net::HTTP.new(uri.host, uri.port)
    @http.use_ssl = true
    escaped = URI.escape("#{@docroot}#{e}")
    logger.debug "Get: #{escaped}"
    @http.start do |http|
      @req = Net::HTTP::Get.new(escaped, {"User-Agent" => @username})
      http_request
    end
    begin
      jresp = JSON @response.body
      if jresp
        return jresp
      end
    rescue JSON::ParserError
      # nothing to do
    end
  end

  # DELETE method
  def delete(e="")
    uri = URI.parse("https://#{@host}/")
    @http = Net::HTTP.new(uri.host, uri.port)
    @http.use_ssl = true
    escaped = URI.escape("#{@docroot}#{e}")
    logger.debug "Delete: #{escaped}"
    @http.start do |http|
      @req = Net::HTTP::Delete.new("#{escaped}",initheader = {"User-Agent" => @username })
      http_request
    end
  end

  # post
  def post(e="")
    uri = URI.parse("https://#{@host}/")
    @http = Net::HTTP.new(uri.host, uri.port)
    @http.use_ssl = true
    escaped = URI.escape("#{@docroot}#{e}")
    logger.debug "Post: #{escaped}"
    @http.start do |http|
      @req = Net::HTTP::Post.new(escaped, {"User-Agent" => @username})
      response = http_request
      logger.debug response
    end
    begin
      jresp = JSON @response.body
      if jresp
        return jresp
      end
    rescue JSON::ParserError
      # nothing to do
    end
  end

  def http_request
    @req.basic_auth(@username, @password)
    @cookie.empty? or @req['Cookie'] = @cookie
    @response = @http.request(@req)
    @response['Set-Cookie'].nil? or @cookie = @response['Set-Cookie']
    if @response.code != "200"
      return "Error status code #{@response.code}\n#{@response.body}"
    end
    logger.debug "Response #{@response.code} #{@response.message}:#{@response.body}"
    return @response.body
  end

  # Post the File contents to the muserver.
  # Default url is template import
  def post_template(filepath="", url="/api/v3/templates/import", content_type=nil)
    doc = File.read(filepath)
    uri = URI.parse("https://#{@host}/")
    logger.debug url
    resp = response = "";
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true
    http.start do |http|
      req = Net::HTTP::Post.new(url, {"User-Agent" => @username})
      req.basic_auth(@username, @password)
      @cookie.empty? or req['Cookie'] = @cookie
      if !content_type.nil?
         req.content_type = content_type
      end
      req.body = doc.to_s
      response = http.request(req)
      response['Set-Cookie'].nil? or @cookie = response['Set-Cookie']
      logger.debug "RESPONSE: #{response.body}"
      if response.code != "200"
        logger.debug "Error status code #{response.code}"
        return "Error status code #{response.code}"
      end
      resp = response.body
    end
    return resp
  end

  # post a test suite csv file
  def post_csv(data, url="/api/v5/ddt/session/test/import/csv")

=begin
    # RestClient attempt: Error 500
    final_url = "https://#{@username}:#{@password}@#{@host}#{url}"
    puts "==> post_csv to #{final_url}"
    RestClient.post(final_url, :myfile => File.new(data))

   #  doc = File.read(data)
   # RestClient.post final_url, doc, :content_type => :csv, :accept => :csv
   # RestClient.post final_url, doc, :content_type => :json, :accept => :json
=end

=begin
    # curb attempt: Error 500
    puts data
    response = ""
    str = "curl -F \"file=@#{data}\" -X POST -k -b cookie_jar --user #{@username}:#{@password} https://#{@host}#{url}"
    Open3.popen3(str) do |input, output, error|
       response = output.read
    end
    return response
=end

=begin
    # Net::Http::Post attempt: Error 415
    xmldoc = nil
    $DEBUG and puts data
    uri = URI.parse("https://#{@host}/")
    resp = response = "";
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true
    http.start do |http|
      req = Net::HTTP::Post.new(url, {"User-Agent" => @username})
      req.basic_auth(@username, @password)
      @cookie.empty? or req['Cookie'] = @cookie
      req.set_form_data({'csv' => data})
     # req.set_form_data({'filename' => data})
     # req.set_form_data({'name' => "file"})
     # req.content_type = "application/octet-stream"
      response = http.request(req)
      $DEBUG and puts "RESPONSE: #{response.body}"
      if response.code != "200"
        $DEBUG and puts "Error status code #{response.code}"
        return "Error status code #{response.code}"
      end
      resp = response.body
    end
    @log and @log.write resp
    if (/<.+>/).match(resp)
      xmldoc = XML::Document.string(resp)
    else
      xmldoc = XML::Document.new
      xmldoc.root = XML::Node.new('err_node', resp)
    end
    return xmldoc
=end
  end

end
